Micron Document
baubs git

Node / mirrors / respira / commits / 381d8e6

Commit 381d8e672d0d6b5513be6f2d22ccdec536dfb7a5


Parents : a3cb6a4
Author : Jan-Henrik Bruhn <jan-henrik.bruhn@offis.de>
Date : 2025-12-26T22:57:40+01:00

fix: Optimize stitch rendering performance for large patterns

Separate static stitch grouping (by color/type) from dynamic completion
status to prevent recalculating all groups on every progress update during
active sewing. This dramatically reduces computational overhead during
500ms polling intervals.

Key optimizations:
- Static groups memo: Only recalculates when stitches/colors change
- Dynamic completion: Only checks group boundaries, not full rebuild
- Custom React.memo comparison: Prevents unnecessary re-renders
- Added comments for future optimization paths (virtualization, LOD, Web Workers)

Performance improvement: O(n) every 500ms -> O(g) where g = number of groups
(typically << n for patterns with multiple colors)

Fixes #32

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Changes

1 files changed, 120 insertions(+), 17 deletions(-)


Diff

diff --git a/src/components/PatternCanvas/KonvaComponents.tsx b/src/components/PatternCanvas/KonvaComponents.tsx
index d75430a..9511729 100644
--- a/src/components/PatternCanvas/KonvaComponents.tsx
+++ b/src/components/PatternCanvas/KonvaComponents.tsx
@@ -157,16 +157,28 @@ export const Stitches = memo(
currentStitchIndex,
showProgress = false,
}: StitchesProps) => {
- const stitchGroups = useMemo(() => {
- interface StitchGroup {
- color: string;
- points: number[];
- completed: boolean;
- isJump: boolean;
- }
+ // PERFORMANCE OPTIMIZATION:
+ // Separate static group structure (doesn't change during sewing)
+ // from dynamic completion status (changes with currentStitchIndex).
+ // This prevents recalculating all groups on every progress update.
+ //
+ // For very large patterns (>100k stitches), consider:
+ // - Virtualization: render only visible stitches based on viewport
+ // - LOD (Level of Detail): reduce stitch density when zoomed out
+ // - Web Workers: offload grouping calculations to background thread
+
+ interface StaticStitchGroup {
+ color: string;
+ points: number[];
+ isJump: boolean;
+ startIndex: number; // First stitch index in this group
+ endIndex: number; // Last stitch index in this group
+ }
- const groups: StitchGroup[] = [];
- let currentGroup: StitchGroup | null = null;
+ // Static grouping - only recalculates when stitches or colors change
+ const staticGroups = useMemo(() => {
+ const groups: StaticStitchGroup[] = [];
+ let currentGroup: StaticStitchGroup | null = null;
let prevX = 0;
let prevY = 0;
@@ -174,37 +186,37 @@ export const Stitches = memo(
for (let i = 0; i < stitches.length; i++) {
const stitch = stitches[i];
const [x, y, cmd, colorIndex] = stitch;
- const isCompleted = i < currentStitchIndex;
const isJump = (cmd & MOVE) !== 0;
const color = getThreadColor(pesData, colorIndex);
- // Start new group if color/status/type changes
+ // Start new group if color or type changes (NOT completion status)
if (
!currentGroup ||
currentGroup.color !== color ||
- currentGroup.completed !== isCompleted ||
currentGroup.isJump !== isJump
) {
- // For jump stitches, we need to create a line from previous position to current position
- // So we include both the previous point and current point
+ // For jump stitches, include previous position
if (isJump && i > 0) {
currentGroup = {
color,
points: [prevX, prevY, x, y],
- completed: isCompleted,
isJump,
+ startIndex: i,
+ endIndex: i,
};
} else {
currentGroup = {
color,
points: [x, y],
- completed: isCompleted,
isJump,
+ startIndex: i,
+ endIndex: i,
};
}
groups.push(currentGroup);
} else {
currentGroup.points.push(x, y);
+ currentGroup.endIndex = i;
}
prevX = x;
@@ -212,7 +224,88 @@ export const Stitches = memo(
}
return groups;
- }, [stitches, pesData, currentStitchIndex]);
+ }, [stitches, pesData]);
+
+ // Dynamic grouping - adds completion status based on currentStitchIndex
+ // Only needs to check group boundaries, not rebuild everything
+ const stitchGroups = useMemo(() => {
+ interface StitchGroup {
+ color: string;
+ points: number[];
+ completed: boolean;
+ isJump: boolean;
+ }
+
+ const groups: StitchGroup[] = [];
+
+ for (const staticGroup of staticGroups) {
+ // Check if this group needs to be split based on completion
+ if (
+ currentStitchIndex > staticGroup.startIndex &&
+ currentStitchIndex <= staticGroup.endIndex
+ ) {
+ // Group is partially completed - need to split
+ // This is rare during sewing (only happens when crossing group boundaries)
+
+ // Rebuild this group with completion split
+ let currentSubGroup: StitchGroup | null = null;
+ const groupStitches = stitches.slice(
+ staticGroup.startIndex,
+ staticGroup.endIndex + 1,
+ );
+
+ let prevX =
+ staticGroup.startIndex > 0
+ ? stitches[staticGroup.startIndex - 1][0]
+ : 0;
+ let prevY =
+ staticGroup.startIndex > 0
+ ? stitches[staticGroup.startIndex - 1][1]
+ : 0;
+
+ for (let i = 0; i < groupStitches.length; i++) {
+ const absoluteIndex = staticGroup.startIndex + i;
+ const stitch = groupStitches[i];
+ const [x, y] = stitch;
+ const isCompleted = absoluteIndex < currentStitchIndex;
+
+ if (!currentSubGroup || currentSubGroup.completed !== isCompleted) {
+ if (staticGroup.isJump && i > 0) {
+ currentSubGroup = {
+ color: staticGroup.color,
+ points: [prevX, prevY, x, y],
+ completed: isCompleted,
+ isJump: staticGroup.isJump,
+ };
+ } else {
+ currentSubGroup = {
+ color: staticGroup.color,
+ points: [x, y],
+ completed: isCompleted,
+ isJump: staticGroup.isJump,
+ };
+ }
+ groups.push(currentSubGroup);
+ } else {
+ currentSubGroup.points.push(x, y);
+ }
+
+ prevX = x;
+ prevY = y;
+ }
+ } else {
+ // Group is fully completed or fully incomplete
+ groups.push({
+ color: staticGroup.color,
+ points: staticGroup.points,
+ completed: currentStitchIndex > staticGroup.endIndex,
+ isJump: staticGroup.isJump,
+ });
+ }
+ }
+
+ return groups;
+ }, [staticGroups, currentStitchIndex, stitches]);
return (
<Group name="stitches">
@@ -239,6 +332,16 @@ export const Stitches = memo(
</Group>
);
},
+ // Custom comparison to prevent unnecessary re-renders
+ (prevProps, nextProps) => {
+ // Re-render only if these values actually changed
+ return (
+ prevProps.stitches === nextProps.stitches &&
+ prevProps.pesData === nextProps.pesData &&
+ prevProps.currentStitchIndex === nextProps.currentStitchIndex &&
+ prevProps.showProgress === nextProps.showProgress
+ );
+ },
);
Stitches.displayName = "Stitches";

Served by rngit 1.3.3 - Generated in 0.04s